from __future__ import absolute_import

import six
from mistune import markdown


from django.core.urlresolvers import reverse
from sentry.models import IntegrationExternalProject, OrganizationIntegration
from sentry.integrations.issues import IssueSyncMixin

from sentry.integrations.exceptions import ApiUnauthorized, ApiError
from django.utils.translation import ugettext as _


class VstsIssueSync(IssueSyncMixin):
    description = 'Integrate Azure DevOps work items by linking a project.'
    slug = 'vsts'
    conf_key = slug

    issue_fields = frozenset(['id', 'title', 'url'])
    done_categories = frozenset(['Resolved', 'Completed'])

    def get_persisted_default_config_fields(self):
        return ['project']

    def create_default_repo_choice(self, default_repo):
        # default_repo should be the project_id
        project = self.get_client().get_project(self.instance, default_repo)
        return (project['id'], project['name'])

    def get_project_choices(self, group, **kwargs):
        client = self.get_client()
        try:
            projects = client.get_projects(self.instance)['value']
        except (ApiError, ApiUnauthorized, KeyError) as e:
            self.raise_error(e)

        project_choices = [(project['id'], project['name']) for project in projects]

        params = kwargs.get('params', {})
        defaults = self.get_project_defaults(group.project_id)
        try:
            default_project = params.get(
                'project', defaults.get('project') or project_choices[0][0])
        except IndexError:
            return None, project_choices

        # If a project has been selected outside of the default list of
        # projects, stick it onto the front of the list so that it can be
        # selected.
        try:
            next(True for r in project_choices if r[0] == default_project)
        except StopIteration:
            try:
                project_choices.insert(0, self.create_default_repo_choice(default_project))
            except (ApiError, ApiUnauthorized):
                return None, project_choices

        return default_project, project_choices

    def get_create_issue_config(self, group, **kwargs):
        kwargs['link_referrer'] = 'vsts_integration'
        fields = super(VstsIssueSync, self).get_create_issue_config(group, **kwargs)
        # Azure/VSTS has BOTH projects and repositories. A project can have many repositories.
        # Workitems (issues) are associated with the project not the repository.
        default_project, project_choices = self.get_project_choices(group, **kwargs)

        return [
            {
                'name': 'project',
                'required': True,
                'type': 'choice',
                'choices': project_choices,
                'defaultValue': default_project,
                'label': _('Project'),
                'placeholder': default_project or _('MyProject'),
            }
        ] + fields

    def get_link_issue_config(self, group, **kwargs):
        fields = super(VstsIssueSync, self).get_link_issue_config(group, **kwargs)
        org = group.organization
        autocomplete_url = reverse(
            'sentry-extensions-vsts-search', args=[org.slug, self.model.id],
        )
        for field in fields:
            if field['name'] == 'externalIssue':
                field['url'] = autocomplete_url
                field['type'] = 'select'
        return fields

    def get_issue_url(self, key, **kwargs):
        return '%s_workitems/edit/%s' % (self.instance, six.text_type(key))

    def create_issue(self, data, **kwargs):
        """
        Creates the issue on the remote service and returns an issue ID.
        """
        project_id = data.get('project')
        if project_id is None:
            raise ValueError('Azure DevOps expects project')

        client = self.get_client()

        title = data['title']
        description = data['description']

        try:
            created_item = client.create_work_item(
                instance=self.instance,
                project=project_id,
                title=title,
                # Decriptions cannot easily be seen. So, a comment will be added as well.
                description=markdown(description),
                comment=markdown(description)
            )
        except Exception as e:
            self.raise_error(e)

        project_name = created_item['fields']['System.AreaPath']
        return {
            'key': six.text_type(created_item['id']),
            'title': title,
            'description': description,
            'metadata': {
                'display_name': '%s#%s' % (project_name, created_item['id']),
            }
        }

    def get_issue(self, issue_id, **kwargs):
        client = self.get_client()
        work_item = client.get_work_item(self.instance, issue_id)
        return {
            'key': six.text_type(work_item['id']),
            'title': work_item['fields']['System.Title'],
            'description': work_item['fields'].get('System.Description'),
            'metadata': {
                'display_name': '%s#%s' % (work_item['fields']['System.AreaPath'], work_item['id']),
            }
        }

    def sync_assignee_outbound(self, external_issue, user, assign=True, **kwargs):
        client = self.get_client()
        assignee = None

        if assign is True:
            sentry_emails = [email.email.lower() for email in user.get_verified_emails()]
            continuation_token = None
            while True:
                vsts_users = client.get_users(self.model.name, continuation_token)
                continuation_token = vsts_users.headers.get('X-MS-ContinuationToken')
                for vsts_user in vsts_users['value']:
                    vsts_email = vsts_user.get(u'mailAddress')
                    if vsts_email and vsts_email.lower() in sentry_emails:
                        assignee = vsts_user['mailAddress']
                        break

                if not continuation_token:
                    break

            if assignee is None:
                # TODO(lb): Email people when this happens
                self.logger.info(
                    'vsts.assignee-not-found',
                    extra={
                        'integration_id': external_issue.integration_id,
                        'user_id': user.id,
                        'issue_key': external_issue.key,
                    }
                )
                return

        try:
            client.update_work_item(
                self.instance, external_issue.key, assigned_to=assignee)
        except (ApiUnauthorized, ApiError):
            self.logger.info(
                'vsts.failed-to-assign',
                extra={
                    'integration_id': external_issue.integration_id,
                    'user_id': user.id,
                    'issue_key': external_issue.key,
                }
            )

    def sync_status_outbound(self, external_issue, is_resolved, project_id, **kwargs):
        client = self.get_client()
        work_item = client.get_work_item(self.instance, external_issue.key)

        # For some reason, vsts doesn't include the project id
        # in the work item response.
        # TODO(jess): figure out if there's a better way to do this
        vsts_project_name = work_item['fields']['System.TeamProject']

        vsts_projects = client.get_projects(self.instance)['value']

        vsts_project_id = None
        for p in vsts_projects:
            if p['name'] == vsts_project_name:
                vsts_project_id = p['id']
                break

        try:
            external_project = IntegrationExternalProject.objects.get(
                external_id=vsts_project_id,
                organization_integration_id__in=OrganizationIntegration.objects.filter(
                    organization_id=external_issue.organization_id,
                    integration_id=external_issue.integration_id,
                )
            )
        except IntegrationExternalProject.DoesNotExist:
            self.logger.info(
                'vsts.external-project-not-found',
                extra={
                    'integration_id': external_issue.integration_id,
                    'is_resolved': is_resolved,
                    'issue_key': external_issue.key,
                }
            )
            return

        status = external_project.resolved_status if \
            is_resolved else external_project.unresolved_status

        try:
            client.update_work_item(
                self.instance, external_issue.key, state=status)
        except (ApiUnauthorized, ApiError) as error:
            self.logger.info(
                'vsts.failed-to-change-status',
                extra={
                    'integration_id': external_issue.integration_id,
                    'is_resolved': is_resolved,
                    'issue_key': external_issue.key,
                    'exception': error,
                }
            )

    def should_unresolve(self, data):
        done_states = self.get_done_states(data['project'])
        return data['old_state'] in done_states or data['old_state'] is None and not data['new_state'] in done_states

    def should_resolve(self, data):
        done_states = self.get_done_states(data['project'])
        return not data['old_state'] in done_states and data['new_state'] in done_states

    def get_done_states(self, project):
        client = self.get_client()
        all_states = client.get_work_item_states(self.instance, project)['value']
        done_states = [
            state['name'] for state in all_states if state['category'] in self.done_categories
        ]
        return done_states

    def get_issue_display_name(self, external_issue):
        if external_issue.metadata is None:
            return ''
        return external_issue.metadata['display_name']
